🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / feature / discovery / src / commonTest / kotlin / org / meshtastic / feature / discovery / DiscoveryScanEngineTest.kt
Displaying Raw • Download
feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt 41414f832f938b655c97bc07509bc140ea16e2a3 (41414f83) Text, 54.16 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tff7b72package T7ee787org.meshtastic.feature.discovery
Tff7b72import T7ee787kotlinx.coroutines.CompletableDeferred
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.async
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.test.TestScope
Tff7b72import T7ee787kotlinx.coroutines.test.UnconfinedTestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.advanceTimeBy
Tff7b72import T7ee787kotlinx.coroutines.test.advanceUntilIdle
Tff7b72import T7ee787kotlinx.coroutines.test.runCurrent
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787okio.ByteString
Tff7b72import T7ee787okio.ByteString.Companion.toByteString
Tff7b72import T7ee787org.meshtastic.core.common.di.ApplicationCoroutineScope
Tff7b72import T7ee787org.meshtastic.core.database.dao.DiscoveryDao
Tff7b72import T7ee787org.meshtastic.core.database.entity.DiscoveredNodeEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.DiscoveryPresetResultEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.DiscoverySessionEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.DiscoverySessionStatus
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.ChannelOption
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DataPacket
Tff7b72import T7ee787org.meshtastic.core.model.MyNodeInfo
Tff7b72import T7ee787org.meshtastic.core.model.Node
Tff7b72import T7ee787org.meshtastic.core.model.NodeAddress
Tff7b72import T7ee787org.meshtastic.core.repository.DiscoveryPacketCollector
Tff7b72import T7ee787org.meshtastic.core.repository.DiscoveryPacketCollectorRegistry
Tff7b72import T7ee787org.meshtastic.core.repository.LocalNodeUnavailableException
Tff7b72import T7ee787org.meshtastic.core.repository.PacketQueueRejectedException
Tff7b72import T7ee787org.meshtastic.core.testing.FakeMeshPrefs
Tff7b72import T7ee787org.meshtastic.core.testing.FakeNodeRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeRadioConfigRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeServiceRepository
Tff7b72import T7ee787org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider
Tff7b72import T7ee787org.meshtastic.proto.Config
Tff7b72import T7ee787org.meshtastic.proto.Data
Tff7b72import T7ee787org.meshtastic.proto.LocalConfig
Tff7b72import T7ee787org.meshtastic.proto.LocalStats
Tff7b72import T7ee787org.meshtastic.proto.MeshPacket
Tff7b72import T7ee787org.meshtastic.proto.PortNum
Tff7b72import T7ee787org.meshtastic.proto.Position
Tff7b72import T7ee787org.meshtastic.proto.Telemetry
Tff7b72import T7ee787org.meshtastic.proto.User
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue
T8b949e// region Inline fakes
T8b949e/** [DiscoveryDao] wrapper that adds deterministic synchronization and failure hooks for scan-engine tests. */
Tff7b72private Tff7b72class T56d364FakeDiscoveryDaoTb4b4b4(Tff7b72private Tff7b72val Te6edf3delegateTb4b4b4: Te6edf3SharedInMemoryDiscoveryDao Tff7b72= Te6edf3SharedInMemoryDiscoveryDaoTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4:
Te6edf3DiscoveryDao Tff7b72by Te6edf3delegate Tb4b4b4{
Tff7b72val Te6edf3sessionsTb4b4b4: Te6edf3MapTff7b72<Tffa657LongTb4b4b4, Te6edf3DiscoverySessionEntityTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3sessions
Tff7b72val Te6edf3presetResultsTb4b4b4: Te6edf3MapTff7b72<Tffa657LongTb4b4b4, Te6edf3DiscoveryPresetResultEntityTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3presetResults
Tff7b72val Te6edf3discoveredNodesTb4b4b4: Te6edf3MapTff7b72<Tffa657LongTb4b4b4, Te6edf3DiscoveredNodeEntityTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3discoveredNodes
Tff7b72var Te6edf3nextInsertSessionEnteredTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3releaseNextInsertSessionTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3nextSessionWriteEnteredTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3releaseNextSessionWriteTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3nextInsertPresetResultEnteredTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3releaseNextInsertPresetResultTb4b4b4: Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3nextSessionWriteFailureTb4b4b4: Te6edf3Exception? Tff7b72= Tff7b72null
Tff7b72suspend Tff7b72fun Td2a8ffseedSessionTb4b4b4(Te6edf3sessionTb4b4b4: Te6edf3DiscoverySessionEntityTb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3seedSessionTb4b4b4(Te6edf3sessionTb4b4b4)
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffinsertSessionTb4b4b4(Te6edf3sessionTb4b4b4: Te6edf3DiscoverySessionEntityTb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Te6edf3nextInsertSessionEnteredTff7b72?.Te6edf3also Tb4b4b4{ Te6edf3entered Tff7b72-Tff7b72>
Te6edf3nextInsertSessionEntered Tff7b72= Tff7b72null
Te6edf3enteredTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}
Te6edf3releaseNextInsertSessionTff7b72?.Te6edf3also Tb4b4b4{ Te6edf3release Tff7b72-Tff7b72>
Te6edf3releaseNextInsertSession Tff7b72= Tff7b72null
Te6edf3releaseTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3delegateTb4b4b4.Te6edf3insertSessionTb4b4b4(Te6edf3sessionTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffapplySessionWriteHooksTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3nextSessionWriteEnteredTff7b72?.Te6edf3also Tb4b4b4{ Te6edf3entered Tff7b72-Tff7b72>
Te6edf3nextSessionWriteEntered Tff7b72= Tff7b72null
Te6edf3enteredTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}
Te6edf3releaseNextSessionWriteTff7b72?.Te6edf3also Tb4b4b4{ Te6edf3release Tff7b72-Tff7b72>
Te6edf3releaseNextSessionWrite Tff7b72= Tff7b72null
Te6edf3releaseTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3nextSessionWriteFailureTff7b72?.Te6edf3also Tb4b4b4{ Te6edf3failure Tff7b72-Tff7b72>
Te6edf3nextSessionWriteFailure Tff7b72= Tff7b72null
Tff7b72throw Te6edf3failure
Tb4b4b4}
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffupdateSessionTb4b4b4(Te6edf3sessionTb4b4b4: Te6edf3DiscoverySessionEntityTb4b4b4) Tb4b4b4{
Te6edf3applySessionWriteHooksTb4b4b4(Tb4b4b4)
Te6edf3delegateTb4b4b4.Te6edf3updateSessionTb4b4b4(Te6edf3sessionTb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffupdateSessionAggregatesTb4b4b4(
Te6edf3sessionIdTb4b4b4: Tffa657LongTb4b4b4,
Te6edf3totalUniqueNodesTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3totalDwellSecondsTb4b4b4: Tffa657LongTb4b4b4,
Te6edf3totalMessagesTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3totalSensorPacketsTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3furthestNodeDistanceTb4b4b4: Tffa657DoubleTb4b4b4,
Te6edf3avgChannelUtilizationTb4b4b4: Tffa657DoubleTb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3applySessionWriteHooksTb4b4b4(Tb4b4b4)
Te6edf3delegateTb4b4b4.Te6edf3updateSessionAggregatesTb4b4b4(
Te6edf3sessionId Tff7b72= Te6edf3sessionIdTb4b4b4,
Te6edf3totalUniqueNodes Tff7b72= Te6edf3totalUniqueNodesTb4b4b4,
Te6edf3totalDwellSeconds Tff7b72= Te6edf3totalDwellSecondsTb4b4b4,
Te6edf3totalMessages Tff7b72= Te6edf3totalMessagesTb4b4b4,
Te6edf3totalSensorPackets Tff7b72= Te6edf3totalSensorPacketsTb4b4b4,
Te6edf3furthestNodeDistance Tff7b72= Te6edf3furthestNodeDistanceTb4b4b4,
Te6edf3avgChannelUtilization Tff7b72= Te6edf3avgChannelUtilizationTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffinsertPresetResultTb4b4b4(Te6edf3resultTb4b4b4: Te6edf3DiscoveryPresetResultEntityTb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Tff7b72val Te6edf3id Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3insertPresetResultTb4b4b4(Te6edf3resultTb4b4b4)
Te6edf3nextInsertPresetResultEnteredTff7b72?.Te6edf3also Tb4b4b4{ Te6edf3entered Tff7b72-Tff7b72>
Te6edf3nextInsertPresetResultEntered Tff7b72= Tff7b72null
Te6edf3enteredTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}
Te6edf3releaseNextInsertPresetResultTff7b72?.Te6edf3also Tb4b4b4{ Te6edf3release Tff7b72-Tff7b72>
Te6edf3releaseNextInsertPresetResult Tff7b72= Tff7b72null
Te6edf3releaseTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3id
Tb4b4b4}
Tb4b4b4}
T8b949e/** Simple fake collector registry that tracks registration. */
Tff7b72private Tff7b72class T56d364FakeCollectorRegistry Tb4b4b4: Te6edf3DiscoveryPacketCollectorRegistry Tb4b4b4{
Tff7b72override Tff7b72var Te6edf3collectorTb4b4b4: Te6edf3DiscoveryPacketCollector? Tff7b72= Tff7b72null
Tb4b4b4}
T8b949e/** AI provider that is never available (no AI in tests). */
Tff7b72private Tff7b72class T56d364FakeAiProvider Tb4b4b4: Te6edf3DiscoverySummaryAiProvider Tb4b4b4{
Tff7b72override Tff7b72val Te6edf3isAvailableTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgenerateSessionSummaryTb4b4b4(
Te6edf3sessionTb4b4b4: Te6edf3DiscoverySessionEntityTb4b4b4,
Te6edf3presetResultsTb4b4b4: Te6edf3ListTff7b72<Te6edf3DiscoveryPresetResultEntityTff7b72>Tb4b4b4,
Tb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Tff7b72null
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgeneratePresetSummaryTb4b4b4(Te6edf3resultTb4b4b4: Te6edf3DiscoveryPresetResultEntityTb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Tff7b72null
Tb4b4b4}
T8b949e// endregion
Tff7b72class T56d364DiscoveryScanEngineTest Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3radioController Tff7b72=
Te6edf3DiscoveryTestRadioControllerTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3selectedDeviceAddress Tff7b72= Te6edf3DEFAULT_DEVICE_ADDRESS Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3serviceRepository Tff7b72= Te6edf3FakeServiceRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3nodeRepository Tff7b72= Te6edf3FakeNodeRepositoryTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3radioConfigRepository Tff7b72=
Te6edf3FakeRadioConfigRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3setLocalConfigDirectTb4b4b4(
Te6edf3LocalConfigTb4b4b4(
Te6edf3lora Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3LoRaConfigTb4b4b4(Te6edf3use_preset Tff7b72= Tff7b72trueTb4b4b4, Te6edf3modem_preset Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3collectorRegistry Tff7b72= Te6edf3FakeCollectorRegistryTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3discoveryDao Tff7b72= Te6edf3FakeDiscoveryDaoTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3aiProvider Tff7b72= Te6edf3FakeAiProviderTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3meshPrefs Tff7b72= Te6edf3FakeMeshPrefsTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3setDeviceAddressTb4b4b4(Te6edf3DEFAULT_DEVICE_ADDRESSTb4b4b4) Tb4b4b4}
T8b949e/** Creates a [DiscoveryScanEngine] wired to test dispatchers sharing the given [testScope]'s scheduler. */
Tff7b72private Tff7b72fun Td2a8ffcreateEngineTb4b4b4(Te6edf3testScopeTb4b4b4: Te6edf3TestScopeTb4b4b4)Tb4b4b4: Te6edf3DiscoveryScanEngine Tb4b4b4{
Tff7b72val Te6edf3testDispatcher Tff7b72= Te6edf3UnconfinedTestDispatcherTb4b4b4(Te6edf3testScopeTb4b4b4.Te6edf3testSchedulerTb4b4b4)
Tff7b72val Te6edf3dispatchers Tff7b72= Te6edf3CoroutineDispatchersTb4b4b4(Te6edf3io Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3main Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3default Tff7b72= Te6edf3testDispatcherTb4b4b4)
Tff7b72val Te6edf3appScope Tff7b72=
Tff7b72object Tb4b4b4: T56d364ApplicationCoroutineScope Tb4b4b4{
Tff7b72override Tff7b72val Te6edf3coroutineContext Tff7b72= Te6edf3testDispatcher Tff7b72+ Te6edf3SupervisorJobTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3DiscoveryScanEngineTb4b4b4(
Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4,
Te6edf3serviceRepository Tff7b72= Te6edf3serviceRepositoryTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3radioConfigRepository Tff7b72= Te6edf3radioConfigRepositoryTb4b4b4,
Te6edf3collectorRegistry Tff7b72= Te6edf3collectorRegistryTb4b4b4,
Te6edf3discoveryDao Tff7b72= Te6edf3discoveryDaoTb4b4b4,
Te6edf3aiProvider Tff7b72= Te6edf3aiProviderTb4b4b4,
Te6edf3applicationScope Tff7b72= Te6edf3appScopeTb4b4b4,
Te6edf3dispatchers Tff7b72= Te6edf3dispatchersTb4b4b4,
Te6edf3meshPrefs Tff7b72= Te6edf3meshPrefsTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3testPresets Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffselectDeviceTb4b4b4(Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4) Tb4b4b4{
Te6edf3meshPrefsTb4b4b4.Te6edf3setDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3selectedDeviceAddress Tff7b72= Te6edf3address
Tb4b4b4}
T8b949e/**
* After [DiscoveryScanEngine.startScan], the state is set to [DiscoveryScanState.Shifting] synchronously. This
* helper asserts that the engine is active — no real-time wait needed.
*/
Tff7b72private Tff7b72fun Td2a8ffassertScanActiveTb4b4b4(Te6edf3engineTb4b4b4: Te6edf3DiscoveryScanEngineTb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4, Ta5d6ff"Ta5d6ffEngine should be active after startScanTa5d6ff"Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Waits briefly for the scan loop (running on test dispatcher) to complete its per-preset initialization
* (collection clearing). Call before sending packets to avoid a race where the scan loop's `collectedNodes.clear()`
* wipes out test-injected data.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffawaitScanLoopInitTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}
T8b949e// region Helper factories
Tff7b72private Tff7b72fun Td2a8ffcreateMyNodeInfoTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657Int Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0Tb4b4b4) Tff7b72= Te6edf3MyNodeInfoTb4b4b4(
Te6edf3myNodeNum Tff7b72= Te6edf3nodeNumTb4b4b4,
Te6edf3hasGPS Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3model Tff7b72= Ta5d6ff"Ta5d6ffTestModelTa5d6ff"Tb4b4b4,
Te6edf3firmwareVersion Tff7b72= Ta5d6ff"Ta5d6ff2.0.0Ta5d6ff"Tb4b4b4,
Te6edf3couldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3shouldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3currentPacketId Tff7b72= T79c0ff1LTb4b4b4,
Te6edf3messageTimeoutMsec Tff7b72= T79c0ff5T79c0ff0T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3minAppVersion Tff7b72= T79c0ff1Tb4b4b4,
Te6edf3maxChannels Tff7b72= T79c0ff8Tb4b4b4,
Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3channelUtilization Tff7b72= T79c0ff0fTb4b4b4,
Te6edf3airUtilTx Tff7b72= T79c0ff0fTb4b4b4,
Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6fftest-deviceTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffcreateNodeWithPositionTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4, Te6edf3latITb4b4b4: Tffa657Int Tff7b72= T79c0ff0Tb4b4b4, Te6edf3lonITb4b4b4: Tffa657Int Tff7b72= T79c0ff0Tb4b4b4) Tff7b72= Te6edf3NodeTb4b4b4(
Te6edf3num Tff7b72= Te6edf3numTb4b4b4,
Te6edf3user Tff7b72= Te6edf3UserTb4b4b4(Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ff!Tffd700${Te6edf3numTb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4, Te6edf3short_name Tff7b72= Ta5d6ff"Ta5d6ffTTffd700$Te6edf3numTa5d6ff"Tb4b4b4, Te6edf3long_name Tff7b72= Ta5d6ff"Ta5d6ffTest Node Tffd700$Te6edf3numTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3position Tff7b72= Te6edf3PositionTb4b4b4(Te6edf3latitude_i Tff7b72= Te6edf3latITb4b4b4, Te6edf3longitude_i Tff7b72= Te6edf3lonITb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffcreatePositionMeshPacketTb4b4b4(
Te6edf3fromTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3latITb4b4b4: Tffa657IntTb4b4b4,
Te6edf3lonITb4b4b4: Tffa657IntTb4b4b4,
Te6edf3snrTb4b4b4: Tffa657Float Tff7b72= T79c0ff5.5fTb4b4b4,
Te6edf3rssiTb4b4b4: Tffa657Int Tff7b72= Tff7b72-T79c0ff7T79c0ff0Tb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3MeshPacket Tb4b4b4{
Tff7b72val Te6edf3posPayload Tff7b72= Te6edf3PositionTb4b4b4.Te6edf3ADAPTERTb4b4b4.Te6edf3encodeTb4b4b4(Te6edf3PositionTb4b4b4(Te6edf3latitude_i Tff7b72= Te6edf3latITb4b4b4, Te6edf3longitude_i Tff7b72= Te6edf3lonITb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toByteStringTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3data Tff7b72= Te6edf3DataTb4b4b4(Te6edf3portnum Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3POSITION_APPTb4b4b4, Te6edf3payload Tff7b72= Te6edf3posPayloadTb4b4b4)
Tff7b72return Te6edf3MeshPacketTb4b4b4(Te6edf3from Tff7b72= Te6edf3fromTb4b4b4, Te6edf3decoded Tff7b72= Te6edf3dataTb4b4b4, Te6edf3rx_snr Tff7b72= Te6edf3snrTb4b4b4, Te6edf3rx_rssi Tff7b72= Te6edf3rssiTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffcreateTelemetryWithLocalStatsTb4b4b4(Te6edf3fromTb4b4b4: Tffa657IntTb4b4b4, Te6edf3localStatsTb4b4b4: Te6edf3LocalStatsTb4b4b4)Tb4b4b4: Te6edf3MeshPacket Tb4b4b4{
Tff7b72val Te6edf3telPayload Tff7b72= Te6edf3TelemetryTb4b4b4.Te6edf3ADAPTERTb4b4b4.Te6edf3encodeTb4b4b4(Te6edf3TelemetryTb4b4b4(Te6edf3local_stats Tff7b72= Te6edf3localStatsTb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toByteStringTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3data Tff7b72= Te6edf3DataTb4b4b4(Te6edf3portnum Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3TELEMETRY_APPTb4b4b4, Te6edf3payload Tff7b72= Te6edf3telPayloadTb4b4b4)
Tff7b72return Te6edf3MeshPacketTb4b4b4(Te6edf3from Tff7b72= Te6edf3fromTb4b4b4, Te6edf3decoded Tff7b72= Te6edf3dataTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffcreateDataPacketTb4b4b4(Te6edf3fromTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3DataPacket Tff7b72= Te6edf3DataPacketTb4b4b4(
Te6edf3to Tff7b72= Te6edf3NodeAddressTb4b4b4.Te6edf3ID_BROADCASTTb4b4b4,
Te6edf3bytes Tff7b72= Te6edf3ByteStringTb4b4b4.Te6edf3EMPTYTb4b4b4,
Te6edf3dataType Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3POSITION_APPTb4b4b4.Te6edf3valueTb4b4b4,
Te6edf3from Tff7b72= Ta5d6ff"Ta5d6ff!Tffd700${Te6edf3fromTb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4,
Te6edf3hopStart Tff7b72= T79c0ff3Tb4b4b4,
Te6edf3hopLimit Tff7b72= T79c0ff3Tb4b4b4,
Tb4b4b4)
T8b949e// endregion
Tf0883e@Test
Tff7b72fun Td2a8ffstartScanCreatesSessionAndRegistersCollectorTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4)
T8b949e// Session should be persisted (happens synchronously inside startScan)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4)
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3IN_PROGRESSTb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3presetsScannedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3homePresetTb4b4b4)
T8b949e// Collector should be registered (synchronous inside startScan)
Te6edf3assertNotNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collector Tff7b72=Tff7b72=Tff7b72= Te6edf3engineTb4b4b4)
T8b949e// currentSession should be populated
Tff7b72val Te6edf3currentSession Tff7b72= Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3value
Te6edf3assertNotNullTb4b4b4(Te6edf3currentSessionTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3sessionTb4b4b4.Te6edf3idTb4b4b4, Te6edf3currentSessionTb4b4b4.Te6edf3idTb4b4b4)
T8b949e// Wait for scan loop to start then clean up
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffdeviceSwitchDuringSessionInsertAbortsScanBeforePublicationTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3firstDevice Tff7b72= Ta5d6ff"Ta5d6ffx:FIRSTTa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3firstDeviceTb4b4b4)
Tff7b72val Te6edf3insertEntered Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseInsert Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextInsertSessionEntered Tff7b72= Te6edf3insertEntered
Te6edf3discoveryDaoTb4b4b4.Te6edf3releaseNextInsertSession Tff7b72= Te6edf3releaseInsert
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3startScan Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4) Tb4b4b4}
Te6edf3insertEnteredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3selectDeviceTb4b4b4(Ta5d6ff"Ta5d6ffx:SECONDTa5d6ff"Tb4b4b4)
Te6edf3releaseInsertTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3startScanTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3FailedTb4b4b4(Ta5d6ff"Ta5d6ffSelected radio changed while preparing the scanTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3valueTb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAborted preparation must not leave an in-progress sessionTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8fftransportRestartDuringSessionInsertAbortsScanBeforePublicationTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3selectDeviceTb4b4b4(Ta5d6ff"Ta5d6ffx:CURRENTTa5d6ff"Tb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3setSessionGenerationTb4b4b4(T79c0ff7LTb4b4b4)
Tff7b72val Te6edf3insertEntered Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseInsert Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextInsertSessionEntered Tff7b72= Te6edf3insertEntered
Te6edf3discoveryDaoTb4b4b4.Te6edf3releaseNextInsertSession Tff7b72= Te6edf3releaseInsert
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3startScan Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4) Tb4b4b4}
Te6edf3insertEnteredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3setSessionGenerationTb4b4b4(T79c0ff8LTb4b4b4)
Te6edf3releaseInsertTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3startScanTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3FailedTb4b4b4(Ta5d6ff"Ta5d6ffSelected radio changed while preparing the scanTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3valueTb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA replaced transport must not publish a prepared sessionTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA replaced transport must not be retunedTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffstopScanPersistsResultsAndTransitionsToIdleTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
T8b949e// Verify scan is active
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
T8b949e// State should be Complete(Cancelled)
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)
Tff7b72val Te6edf3completeState Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3Complete
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3CancelledTb4b4b4, Te6edf3completeStateTb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)
T8b949e// Collector should be unregistered
Te6edf3assertNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4)
T8b949e// stopScan returns after process-scope restoration ownership is established, not after its settle delay.
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3RESTORE_PENDING_STOPPEDTb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3advanceTimeByTb4b4b4(Te6edf3DiscoveryHomeRestorerTb4b4b4.Te6edf3POST_RESTORE_SETTLE_DELAY_MSTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3STOPPEDTb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffcompleteScanCreatesSessionWithInProgressStatusTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff5Tb4b4b4)
T8b949e// Immediately after startScan, the session should exist with "in_progress"
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3IN_PROGRESSTb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3completionStatusTb4b4b4)
T8b949e// Wait for the scan loop to start, then verify active
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffemptyPresetDwellPersistsZeroResultEntryTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
T8b949e// Stop without receiving any packets — forces persistCurrentDwellResults
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
T8b949e// Should have a preset result with zero unique nodes
Tff7b72val Te6edf3presetResults Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3presetResultsTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffExpected at least one preset resultTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3presetResultsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3presetNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3uniqueNodesTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3messageCountTb4b4b4)
T8b949e// No discovered nodes
Te6edf3assertTrueTb4b4b4(Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffpacketCollectionPopulatesNodeDataTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3myNodeNum Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNumTb4b4b4)Tb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setNodesTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3createNodeWithPositionTb4b4b4(Te6edf3num Tff7b72= Te6edf3myNodeNumTb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff0T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff0T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
T8b949e// Wait for Dwell state
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3DwellTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}
T8b949e// Simulate receiving a position packet
Tff7b72val Te6edf3meshPacket Tff7b72=
Te6edf3createPositionMeshPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff3T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3snr Tff7b72= T79c0ff5.5fTb4b4b4, Te6edf3rssi Tff7b72= Tff7b72-T79c0ff7T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3dataPacket Tff7b72= Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3dataPacketTb4b4b4)
T8b949e// Stop scan to persist results
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
T8b949e// Should have one discovered node with lat/lon
Tff7b72val Te6edf3nodes Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3nodesTb4b4b4.Te6edf3sizeTb4b4b4)
Tff7b72val Te6edf3node Tff7b72= Te6edf3nodesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5LTb4b4b4, Te6edf3nodeTb4b4b4.Te6edf3nodeNumTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3latitudeTb4b4b4, Ta5d6ff"Ta5d6ffNode should have latitudeTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3longitudeTb4b4b4, Ta5d6ff"Ta5d6ffNode should have longitudeTa5d6ff"Tb4b4b4)
T8b949e// latitude_i = 377749300 → 37.77493
Te6edf3assertTrueTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3latitudeTff7b72!! Tff7b72> T79c0ff3T79c0ff7.7 Tff7b72&Tff7b72& Te6edf3nodeTb4b4b4.Te6edf3latitudeTff7b72!! Tff7b72< T79c0ff3T79c0ff7.8Tb4b4b4, Ta5d6ff"Ta5d6ffLatitude should be ~37.77Ta5d6ff"Tb4b4b4)
T8b949e// longitude_i = -1224194200 → -122.41942
Te6edf3assertTrueTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3longitudeTff7b72!! Tff7b72< Tff7b72-T79c0ff1T79c0ff2T79c0ff2.4 Tff7b72&Tff7b72& Te6edf3nodeTb4b4b4.Te6edf3longitudeTff7b72!! Tff7b72> Tff7b72-T79c0ff1T79c0ff2T79c0ff2.5Tb4b4b4, Ta5d6ff"Ta5d6ffLongitude should be ~-122.42Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff5.5fTb4b4b4, Te6edf3nodeTb4b4b4.Te6edf3snrTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72-T79c0ff7T79c0ff0Tb4b4b4, Te6edf3nodeTb4b4b4.Te6edf3rssiTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffreportedZeroRssiIsRecordedRatherThanDiscardedTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3DwellTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}
T8b949e// 0 dBm is a legitimate reading on some radios, not a stand-in for "no reading".
Tff7b72val Te6edf3meshPacket Tff7b72=
Te6edf3createPositionMeshPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff3T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3snr Tff7b72= T79c0ff5.5fTb4b4b4, Te6edf3rssi Tff7b72= T79c0ff0Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3rssiTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffabsentRssiPersistsAsNullRatherThanZeroTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3DwellTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3posPayload Tff7b72= Te6edf3PositionTb4b4b4.Te6edf3ADAPTERTb4b4b4.Te6edf3encodeTb4b4b4(Te6edf3PositionTb4b4b4(Te6edf3latitude_i Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff3T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toByteStringTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3meshPacket Tff7b72=
Te6edf3MeshPacketTb4b4b4(
Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4,
Te6edf3decoded Tff7b72= Te6edf3DataTb4b4b4(Te6edf3portnum Tff7b72= Te6edf3PortNumTb4b4b4.Te6edf3POSITION_APPTb4b4b4, Te6edf3payload Tff7b72= Te6edf3posPayloadTb4b4b4)Tb4b4b4,
Te6edf3rx_snr Tff7b72= T79c0ff5.5fTb4b4b4,
Te6edf3rx_rssi Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3rssiTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8fftelemetryWithLocalStatsPopulatesRfHealthTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
T8b949e// Wait for Dwell state and ensure sessionId is set
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3Dwell Tff7b72|Tff7b72| Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}
T8b949e// Send a telemetry packet with local_stats
Tff7b72val Te6edf3localStats Tff7b72=
Te6edf3LocalStatsTb4b4b4(
Te6edf3num_packets_tx Tff7b72= T79c0ff1T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3num_packets_rx Tff7b72= T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3num_packets_rx_bad Tff7b72= T79c0ff5Tb4b4b4,
Te6edf3num_rx_dupe Tff7b72= T79c0ff1T79c0ff0Tb4b4b4,
Te6edf3num_tx_relay Tff7b72= T79c0ff1T79c0ff5Tb4b4b4,
Te6edf3num_tx_relay_canceled Tff7b72= T79c0ff2Tb4b4b4,
Te6edf3num_online_nodes Tff7b72= T79c0ff3Tb4b4b4,
Te6edf3num_total_nodes Tff7b72= T79c0ff1T79c0ff0Tb4b4b4,
Te6edf3uptime_seconds Tff7b72= T79c0ff3T79c0ff6T79c0ff0T79c0ff0Tb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3meshPacket Tff7b72= Te6edf3createTelemetryWithLocalStatsTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4, Te6edf3localStats Tff7b72= Te6edf3localStatsTb4b4b4)
Tff7b72val Te6edf3dataPacket Tff7b72= Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4T79c0ff5Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3dataPacketTb4b4b4)
T8b949e// Stop to persist
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
T8b949e// The preset result should have RF health fields from local_stats
Tff7b72val Te6edf3presetResults Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3presetResultsTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffExpected a preset resultTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3presetResultsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numPacketsTxTb4b4b4, Ta5d6ff"Ta5d6ffnumPacketsTx should be 100Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numPacketsRxTb4b4b4, Ta5d6ff"Ta5d6ffnumPacketsRx should be 200Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff5Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numPacketsRxBadTb4b4b4, Ta5d6ff"Ta5d6ffnumPacketsRxBad should be 5Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numRxDupeTb4b4b4, Ta5d6ff"Ta5d6ffnumRxDupe should be 10Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff5Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numTxRelayTb4b4b4, Ta5d6ff"Ta5d6ffnumTxRelay should be 15Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numTxRelayCanceledTb4b4b4, Ta5d6ff"Ta5d6ffnumTxRelayCanceled should be 2Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numOnlineNodesTb4b4b4, Ta5d6ff"Ta5d6ffnumOnlineNodes should be 3Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3numTotalNodesTb4b4b4, Ta5d6ff"Ta5d6ffnumTotalNodes should be 10Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3T79c0ff6T79c0ff0T79c0ff0Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3uptimeSecondsTb4b4b4, Ta5d6ff"Ta5d6ffuptimeSeconds should be 3600Ta5d6ff"Tb4b4b4)
T8b949e// Packet success/failure rates should be computed
T8b949e// success = (200 - 5) / 200 * 100 = 97.5
T8b949e// failure = 5 / 200 * 100 = 2.5
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3packetSuccessRate Tff7b72> T79c0ff9T79c0ff7.0Tb4b4b4, Ta5d6ff"Ta5d6ffSuccess rate should be ~97.5%Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3packetFailureRate Tff7b72> T79c0ff2.0Tb4b4b4, Ta5d6ff"Ta5d6ffFailure rate should be ~2.5%Ta5d6ff"Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffuserPositionCapturedAtScanStartTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3myNodeNum Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNumTb4b4b4)Tb4b4b4)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setNodesTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3createNodeWithPositionTb4b4b4(Te6edf3num Tff7b72= Te6edf3myNodeNumTb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff3T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
T8b949e// User position should be captured from the own node
T8b949e// latitude_i = 377749300 → 37.77493
Te6edf3assertTrueTb4b4b4(Te6edf3sessionTb4b4b4.Te6edf3userLatitude Tff7b72> T79c0ff3T79c0ff7.7 Tff7b72&Tff7b72& Te6edf3sessionTb4b4b4.Te6edf3userLatitude Tff7b72< T79c0ff3T79c0ff7.8Tb4b4b4, Ta5d6ff"Ta5d6ffUser lat should be ~37.77Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3sessionTb4b4b4.Te6edf3userLongitude Tff7b72< Tff7b72-T79c0ff1T79c0ff2T79c0ff2.4 Tff7b72&Tff7b72& Te6edf3sessionTb4b4b4.Te6edf3userLongitude Tff7b72> Tff7b72-T79c0ff1T79c0ff2T79c0ff2.5Tb4b4b4, Ta5d6ff"Ta5d6ffUser lon should be ~-122.42Ta5d6ff"Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffdistanceFromUserCalculatedForDiscoveredNodesTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3myNodeNum Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNumTb4b4b4)Tb4b4b4)
T8b949e// User at San Francisco (37.7749, -122.4194)
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setNodesTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3createNodeWithPositionTb4b4b4(Te6edf3num Tff7b72= Te6edf3myNodeNumTb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff7T79c0ff7T79c0ff4T79c0ff9T79c0ff0T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff4T79c0ff1T79c0ff9T79c0ff4T79c0ff0T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4)Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
T8b949e// Wait for Dwell state
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3DwellTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}
T8b949e// Discovered node at Oakland (37.8044, -122.2712) — roughly 15 km away
Tff7b72val Te6edf3meshPacket Tff7b72= Te6edf3createPositionMeshPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff5T79c0ff4T79c0ff3T79c0ff2T79c0ff1Tb4b4b4, Te6edf3latI Tff7b72= T79c0ff3T79c0ff7T79c0ff8T79c0ff0T79c0ff4T79c0ff4T79c0ff0T79c0ff0T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= Tff7b72-T79c0ff1T79c0ff2T79c0ff2T79c0ff2T79c0ff7T79c0ff1T79c0ff2T79c0ff0T79c0ff0T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3dataPacket Tff7b72= Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff5T79c0ff4T79c0ff3T79c0ff2T79c0ff1Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(Te6edf3meshPacketTb4b4b4, Te6edf3dataPacketTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3nodes Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3nodesTb4b4b4.Te6edf3sizeTb4b4b4)
Tff7b72val Te6edf3node Tff7b72= Te6edf3nodesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTb4b4b4, Ta5d6ff"Ta5d6ffDistance from user should be computedTa5d6ff"Tb4b4b4)
T8b949e// SF to Oakland is roughly 13–17 km
Te6edf3assertTrueTb4b4b4(
Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTff7b72!! Tff7b72> T79c0ff1T79c0ff0Te6edf3_000 Tff7b72&Tff7b72& Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTff7b72!! Tff7b72< T79c0ff2T79c0ff5Te6edf3_000Tb4b4b4,
Ta5d6ff"Ta5d6ffDistance should be between 10km and 25km, was Tffd700${Te6edf3nodeTb4b4b4.Te6edf3distanceFromUserTffd700}Ta5d6ffmTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffneighborRequestQueueRejectionKeepsBestEffortScanRunningTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3requestNeighborInfoFailure Tff7b72= Te6edf3PacketQueueRejectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffNeighbor infoTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3SuccessTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3neighborInfoRequestsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffcompleteTa5d6ff"Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3singleTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8fflocalIdentityLossDuringNeighborRequestKeepsBestEffortScanRunningTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3nodeRepositoryTb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(Te6edf3createMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3requestNeighborInfoFailure Tff7b72= Te6edf3LocalNodeUnavailableExceptionTb4b4b4(Ta5d6ff"Ta5d6ffNeighbor infoTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3SuccessTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3neighborInfoRequestsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffcompleteTa5d6ff"Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3singleTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Tb4b4b4}
T8b949e// region Home-preset restoration (the one config-mutating, safety-critical behavior of a scan)
Tf0883e@Test
Tff7b72fun Td2a8ffreconnectTimeoutAbortsRestoresHomePresetAndFinalizesSessionTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
T8b949e// Scan a preset different from the seeded home (LONG_FAST) so a restore-to-home is observable.
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
T8b949e// The radio fails to come back after the preset shift (firmware reboots on a LoRa config change).
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
T8b949e// The abort path must reach a terminal Failed state, finalize the session, and restore the home preset —
T8b949e// none of which happened before the fix, because cancelScanInternal() cancelled this coroutine first.
Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3FailedTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3isActiveTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3collectorRegistryTb4b4b4.Te6edf3collectorTb4b4b4, Ta5d6ff"Ta5d6ffcollector should be unregisteredTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3RESTORE_PENDING_FAILEDTb4b4b4,
Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3FAILEDTb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffdwellFailurePersistsNodesCollectedBeforeTheAbortTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Tff7b72while Tb4b4b4(Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value Tff7b72!is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3DwellTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(T79c0ff1T79c0ff0T79c0ff0Tb4b4b4)
Tb4b4b4}
Te6edf3engineTb4b4b4.Te6edf3onPacketReceivedTb4b4b4(
Te6edf3createPositionMeshPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff5T79c0ff4T79c0ff3T79c0ff2T79c0ff1Tb4b4b4, Te6edf3latI Tff7b72= T79c0ff0Tb4b4b4, Te6edf3lonI Tff7b72= T79c0ff0Tb4b4b4)Tb4b4b4,
Te6edf3createDataPacketTb4b4b4(Te6edf3from Tff7b72= T79c0ff5T79c0ff4T79c0ff3T79c0ff2T79c0ff1Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3singleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4.Te6edf3nameTb4b4b4, Te6edf3resultTb4b4b4.Te6edf3presetNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3resultTb4b4b4.Te6edf3uniqueNodesTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3discoveredNodesTb4b4b4.Te6edf3sizeTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffstopScanRestoresHomePresetTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4) T8b949e// let the applicationScope restore complete
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffconcurrentStopsShareOneTerminalCleanupAndHomeRestoreTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Tff7b72val Te6edf3originalSessionId Tff7b72= Te6edf3checkNotNullTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3valueTb4b4b4)Tb4b4b4.Te6edf3id
Tff7b72val Te6edf3updateEntered Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseUpdate Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextSessionWriteEntered Tff7b72= Te6edf3updateEntered
Te6edf3discoveryDaoTb4b4b4.Te6edf3releaseNextSessionWrite Tff7b72= Te6edf3releaseUpdate
Tff7b72val Te6edf3firstStop Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3updateEnteredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3redundantStop Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3redundantStopTb4b4b4.Te6edf3isCompletedTb4b4b4, Ta5d6ff"Ta5d6ffall stop callers must await the elected terminal cleanupTa5d6ff"Tb4b4b4)
Te6edf3releaseUpdateTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3firstStopTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3redundantStopTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3homeWrites Tff7b72=
Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3count Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3loraTff7b72?.Te6edf3modem_preset Tff7b72=Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPreset Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3homeWritesTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffjoined terminal cleanup must persist the dwell exactly onceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3setOfTb4b4b4(Te6edf3originalSessionIdTb4b4b4)Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3keysTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffstopRacingLastDwellPersistenceDoesNotInsertTheResultTwiceTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3insertEntered Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseInsert Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextInsertPresetResultEntered Tff7b72= Te6edf3insertEntered
Te6edf3discoveryDaoTb4b4b4.Te6edf3releaseNextInsertPresetResult Tff7b72= Te6edf3releaseInsert
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_001LTb4b4b4)
Te6edf3insertEnteredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3stop Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3stopTb4b4b4.Te6edf3isCompletedTb4b4b4, Ta5d6ff"Ta5d6ffstop must serialize with the final dwell persistenceTa5d6ff"Tb4b4b4)
Te6edf3releaseInsertTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3stopTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3sizeTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffstopDuringNaturalTerminalCleanupDoesNotReplaceSuccessfulCompletionTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3updateEntered Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseUpdate Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextSessionWriteEntered Tff7b72= Te6edf3updateEntered
Te6edf3discoveryDaoTb4b4b4.Te6edf3releaseNextSessionWrite Tff7b72= Te6edf3releaseUpdate
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff1Te6edf3_001LTb4b4b4)
Te6edf3updateEnteredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3AnalysisTb4b4b4, Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3releaseUpdateTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3SuccessTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3COMPLETETb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffterminalAggregateWriteDoesNotOverwriteAConcurrentRestoreStatusTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Tff7b72val Te6edf3sessionId Tff7b72= Te6edf3checkNotNullTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3valueTb4b4b4)Tb4b4b4.Te6edf3id
Tff7b72val Te6edf3updateEntered Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseUpdate Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextSessionWriteEntered Tff7b72= Te6edf3updateEntered
Te6edf3discoveryDaoTb4b4b4.Te6edf3releaseNextSessionWrite Tff7b72= Te6edf3releaseUpdate
Tff7b72val Te6edf3stop Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3updateEnteredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3updateRecoverableSessionCompletionStatusTb4b4b4(Te6edf3sessionIdTb4b4b4, Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3COMPLETETb4b4b4)
Te6edf3releaseUpdateTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3stopTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3COMPLETETb4b4b4,
Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(Te6edf3sessionIdTb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4,
Ta5d6ff"Ta5d6ffterminal aggregate persistence must preserve a status finalized by a concurrent restoreTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffresetAndNewScanCannotDisplaceActiveTerminalCleanupTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Tff7b72val Te6edf3originalSessionId Tff7b72= Te6edf3assertNotNullTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3valueTb4b4b4)Tb4b4b4.Te6edf3id
Tff7b72val Te6edf3updateEntered Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseUpdate Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextSessionWriteEntered Tff7b72= Te6edf3updateEntered
Te6edf3discoveryDaoTb4b4b4.Te6edf3releaseNextSessionWrite Tff7b72= Te6edf3releaseUpdate
Tff7b72val Te6edf3stop Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3updateEnteredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3resetTb4b4b4(Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3MEDIUM_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3setOfTb4b4b4(Te6edf3originalSessionIdTb4b4b4)Tb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3keysTb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3IN_PROGRESSTb4b4b4,
Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(Te6edf3originalSessionIdTb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CancellingTb4b4b4, Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3originalSessionIdTb4b4b4,
Te6edf3assertNotNullTb4b4b4(Te6edf3engineTb4b4b4.Te6edf3currentSessionTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffreset must not discard the active session restore planTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3idTb4b4b4,
Tb4b4b4)
Te6edf3assertFalseTb4b4b4(
Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3loraTff7b72?.Te6edf3modem_preset Tff7b72=Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3MEDIUM_FASTTb4b4b4.Te6edf3modemPreset Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffa refused restart must not retune the radioTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3releaseUpdateTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3stopTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8fftargetShiftFailureFailsScanAndStillRestoresHomePresetTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3radioControllerTb4b4b4.Te6edf3failLocalConfigWritesRemaining Tff7b72= T79c0ff1
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3FailedTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3FAILEDTb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3discoveryDaoTb4b4b4.Te6edf3presetResultsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffa shift failure runs no dwell, so it must not insert an empty preset resultTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffstopScanRetriesHomeRestoreAfterTransientWriteFailureTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3failLocalConfigWritesRemaining Tff7b72= T79c0ff1
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3failLocalConfigWritesRemainingTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffnewScanWaitsForPendingHomeRestoreBeforeRetuningTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3failLocalConfigWritesRemaining Tff7b72= T79c0ff1
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3resetTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3restart Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3MEDIUM_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4) Tb4b4b4}
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3restartTb4b4b4.Te6edf3isCompletedTb4b4b4, Ta5d6ff"Ta5d6ffa new scan must wait for the prior session's home restoreTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(
Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3loraTff7b72?.Te6edf3modem_preset Tff7b72=Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3MEDIUM_FASTTb4b4b4.Te6edf3modemPreset Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffthe new scan must not retune before the prior home restore succeedsTa5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// The home restorer's retry delay after the failed config write, plus the settle delay after the
T8b949e// successful restore. Both must elapse before awaitBeforeScan admits the new scan.
Te6edf3advanceTimeByTb4b4b4(Te6edf3DiscoveryHomeRestorerTb4b4b4.Te6edf3RETRY_DELAY_MS Tff7b72+ Te6edf3DiscoveryHomeRestorerTb4b4b4.Te6edf3POST_RESTORE_SETTLE_DELAY_MS Tff7b72+ T79c0ff1Tb4b4b4)
Te6edf3restartTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3appliedPresets Tff7b72= Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3loraTff7b72?.Te6edf3modem_preset Tb4b4b4}
Tff7b72val Te6edf3homeRestoreIndex Tff7b72= Te6edf3appliedPresetsTb4b4b4.Te6edf3indexOfFirst Tb4b4b4{ Tffa657it Tff7b72=Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPreset Tb4b4b4}
Tff7b72val Te6edf3newScanIndex Tff7b72= Te6edf3appliedPresetsTb4b4b4.Te6edf3indexOfFirst Tb4b4b4{ Tffa657it Tff7b72=Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3MEDIUM_FASTTb4b4b4.Te6edf3modemPreset Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3homeRestoreIndex Tff7b72>Tff7b72= T79c0ff0Tb4b4b4, Ta5d6ff"Ta5d6ffthe prior session must restore the home presetTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3newScanIndex Tff7b72> Te6edf3homeRestoreIndexTb4b4b4, Ta5d6ff"Ta5d6ffthe new scan retune must happen after home restorationTa5d6ff"Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffscanIsRefusedWhenHomeLoraConfigCannotBeCapturedTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3setLocalConfigDirectTb4b4b4(Te6edf3LocalConfigTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3FailedTb4b4b4(Ta5d6ff"Ta5d6ffHome LoRa configuration is not availableTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffscanIsRefusedWithoutSelectedDeviceOwnershipTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3selectDeviceTb4b4b4(Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3FailedTb4b4b4(Ta5d6ff"Ta5d6ffSelected radio is not availableTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3valueTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffterminalPersistenceFailureCannotSuppressHomeRestorationTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3discoveryDaoTb4b4b4.Te6edf3nextSessionWriteFailure Tff7b72= Te6edf3IllegalStateExceptionTb4b4b4(Ta5d6ff"Ta5d6ffdatabase unavailableTa5d6ff"Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3CancelledTb4b4b4)Tb4b4b4,
Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3valueTb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3STOPPEDTb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3singleTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffdeviceSwitchCancelsOldProcessRestoreWithoutApplyingItToReplacementTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3firstDevice Tff7b72= Ta5d6ff"Ta5d6ffx:FIRSTTa5d6ff"
Tff7b72val Te6edf3secondDevice Tff7b72= Ta5d6ff"Ta5d6ffx:SECONDTa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3firstDeviceTb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3resetTb4b4b4(Tb4b4b4)
Te6edf3selectDeviceTb4b4b4(Te6edf3secondDeviceTb4b4b4)
Tff7b72val Te6edf3secondDeviceHome Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_SLOW
Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3setLocalConfigDirectTb4b4b4(
Te6edf3LocalConfigTb4b4b4(Te6edf3lora Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3LoRaConfigTb4b4b4(Te6edf3use_preset Tff7b72= Tff7b72trueTb4b4b4, Te6edf3modem_preset Tff7b72= Te6edf3secondDeviceHomeTb4b4b4.Te6edf3modemPresetTb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3MEDIUM_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3presets Tff7b72= Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3loraTff7b72?.Te6edf3modem_preset Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3MEDIUM_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3presetsTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertFalseTb4b4b4(
Te6edf3presetsTb4b4b4.Te6edf3containsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffthe first device's delayed home restore must not retune the replacement deviceTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3finalPresets Tff7b72= Te6edf3radioControllerTb4b4b4.Te6edf3configWritesTb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3loraTff7b72?.Te6edf3modem_preset Tb4b4b4}
Te6edf3assertFalseTb4b4b4(
Te6edf3finalPresetsTb4b4b4.Te6edf3containsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffthe first device's restore must stay rejected after the deferred attempt runsTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3secondDeviceHomeTb4b4b4.Te6edf3modemPresetTb4b4b4,
Te6edf3finalPresetsTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffthe replacement device must restore its own captured home presetTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffnormalCompletionRestoresHomePresetTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3SHORT_FASTTb4b4b4)Tb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4) T8b949e// connection stays Connected, so the scan runs to completion
Tff7b72val Te6edf3state Tff7b72= Te6edf3engineTb4b4b4.Te6edf3scanStateTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3state Tff7b72is Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4, Ta5d6ff"Ta5d6ffexpected Complete, was Tffd700$Te6edf3stateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompletionOutcomeTb4b4b4.Te6edf3SuccessTb4b4b4, Tb4b4b4(Te6edf3state Tff7b72as Te6edf3DiscoveryScanStateTb4b4b4.Te6edf3CompleteTb4b4b4)Tb4b4b4.Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3COMPLETETb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Tb4b4b4}
T8b949e// endregion
T8b949e// region Interrupted-session detection and restore (crash / BLE-loss recovery)
T8b949e/** Seeds a prior-process interrupted session directly into the fake DAO (keyed by its own id). */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffseedInterruptedSessionTb4b4b4(
Te6edf3idTb4b4b4: Tffa657Long Tff7b72= T79c0ff1LTb4b4b4,
Te6edf3deviceAddressTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3homePresetTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4,
Te6edf3completionStatusTb4b4b4: Tffa657String Tff7b72= Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3IN_PROGRESSTb4b4b4,
Te6edf3homeLoraConfigTb4b4b4: Te6edf3ConfigTb4b4b4.Te6edf3LoRaConfigTff7b72? Tff7b72=
Te6edf3ConfigTb4b4b4.Te6edf3LoRaConfigTb4b4b4(Te6edf3use_preset Tff7b72= Tff7b72trueTb4b4b4, Te6edf3modem_preset Tff7b72= Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3discoveryDaoTb4b4b4.Te6edf3seedSessionTb4b4b4(
Te6edf3DiscoverySessionEntityTb4b4b4(
Te6edf3id Tff7b72= Te6edf3idTb4b4b4,
Te6edf3timestamp Tff7b72= T79c0ff1LTb4b4b4,
Te6edf3presetsScanned Tff7b72= Ta5d6ff"Ta5d6ffSHORT_FASTTa5d6ff"Tb4b4b4,
Te6edf3homePreset Tff7b72= Te6edf3homePresetTb4b4b4,
Te6edf3completionStatus Tff7b72= Te6edf3completionStatusTb4b4b4,
Te6edf3deviceAddress Tff7b72= Te6edf3deviceAddressTb4b4b4,
Te6edf3homeLoraConfig Tff7b72= Te6edf3homeLoraConfigTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffstartScanCapturesDeviceAddressAndHomeConfigForLaterRestoreTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3selectDeviceTb4b4b4(Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff1T79c0ff0Tb4b4b4)
Tff7b72val Te6edf3session Tff7b72= Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"Tb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3deviceAddressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3sessionTb4b4b4.Te6edf3homeLoraConfigTff7b72?.Te6edf3modem_presetTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffrestoreInterruptedSessionsOnReconnectRestoresMatchingDeviceTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3addressTb4b4b4)
T8b949e// A previous process's scan died mid-flight and never reached restoreHomePreset/finalizeSession.
Te6edf3seedInterruptedSessionTb4b4b4(Te6edf3deviceAddress Tff7b72= Te6edf3addressTb4b4b4)
Tff7b72val Te6edf3restored Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3watcherJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{ Te6edf3restored Tff7b72+Tff7b72= Tffa657it Tb4b4b4} Tb4b4b4}
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4) T8b949e// serviceRepository is already Connected, so the watcher's first collection fires now
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3RESTOREDTb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3restoredTb4b4b4, Ta5d6ff"Ta5d6ffCaller should be notified with the restored home presetTa5d6ff"Tb4b4b4)
Te6edf3watcherJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffrestoreInterruptedSessionsOnReconnectIgnoresDifferentDeviceTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3selectDeviceTb4b4b4(Ta5d6ff"Ta5d6ffx:CURRENTTa5d6ff"Tb4b4b4)
Te6edf3seedInterruptedSessionTb4b4b4(Te6edf3deviceAddress Tff7b72= Ta5d6ff"Ta5d6ffx:OTHER-DEVICETa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3restored Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3watcherJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{ Te6edf3restored Tff7b72+Tff7b72= Tffa657it Tb4b4b4} Tb4b4b4}
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTb4b4b4, Ta5d6ff"Ta5d6ffShould not touch the radio for a session from another deviceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3IN_PROGRESSTb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3restoredTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffNo notification for a session from another deviceTa5d6ff"Tb4b4b4)
Te6edf3watcherJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffrestoreInterruptedSessionsOnReconnectSkipsWhileScanActiveTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3addressTb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3startScanTb4b4b4(Te6edf3testPresetsTb4b4b4, Te6edf3dwellDurationSeconds Tff7b72= T79c0ff6T79c0ff0Tb4b4b4)
Te6edf3assertScanActiveTb4b4b4(Te6edf3engineTb4b4b4)
T8b949e// A stale session from a *different*, already-dead process — seeded at a key the active scan's own
T8b949e// insertSession call (id=1) won't collide with, so this row's fate isolates what the watcher itself does.
Te6edf3seedInterruptedSessionTb4b4b4(Te6edf3id Tff7b72= T79c0ff9T79c0ff9T79c0ff9Tb4b4b4, Te6edf3deviceAddress Tff7b72= Te6edf3addressTb4b4b4)
T8b949e// The connection is already Connected, so the watcher fires on its very first emission — while this engine's
T8b949e// own scan is still active. runCurrent (not advanceUntilIdle) drains that emission WITHOUT advancing virtual
T8b949e// time, so the scan's dwell delays never elapse and it stays active for the duration of the check.
Tff7b72val Te6edf3restored Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3watcherJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{ Te6edf3restored Tff7b72+Tff7b72= Tffa657it Tb4b4b4} Tb4b4b4}
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3IN_PROGRESSTb4b4b4,
Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(T79c0ff9T79c0ff9T79c0ff9Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4,
Ta5d6ff"Ta5d6ffStale row must not be touched while this engine's own scan is activeTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3restoredTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffNo restore notification while this engine's own scan is activeTa5d6ff"Tb4b4b4)
Te6edf3watcherJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3engineTb4b4b4.Te6edf3stopScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffrestoreWatcherSurvivesWriteFailureAndRetriesOnNextReconnectTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3addressTb4b4b4)
Te6edf3seedInterruptedSessionTb4b4b4(Te6edf3deviceAddress Tff7b72= Te6edf3addressTb4b4b4)
T8b949e// The link drops right after Connected, so the restore's config write fails.
Te6edf3radioControllerTb4b4b4.Te6edf3throwOnSetLocalConfig Tff7b72= Tff7b72true
Te6edf3radioControllerTb4b4b4.Te6edf3onLocalConfigWriteAttempt Tff7b72= Tb4b4b4{
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3onLocalConfigWriteAttempt Tff7b72= Tff7b72null
Tb4b4b4}
Tff7b72val Te6edf3restored Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3watcherJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{ Te6edf3restored Tff7b72+Tff7b72= Tffa657it Tb4b4b4} Tb4b4b4}
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3IN_PROGRESSTb4b4b4,
Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4,
Ta5d6ff"Ta5d6ffFailed restore must not mark the session restoredTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3restoredTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffNo notification when the restore write failedTa5d6ff"Tb4b4b4)
T8b949e// Next reconnect succeeds — the watcher must still be alive to retry. A real reconnect always passes through
T8b949e// Disconnected first; the intermediate advanceUntilIdle lets the watcher observe the drop, so the return to
T8b949e// Connected is a genuine transition rather than a conflated no-op the StateFlow would dedupe away.
Te6edf3radioControllerTb4b4b4.Te6edf3throwOnSetLocalConfig Tff7b72= Tff7b72false
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3RESTOREDTb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ChannelOptionTb4b4b4.Te6edf3LONG_FASTTb4b4b4.Te6edf3modemPresetTb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTff7b72?.Te6edf3loraTff7b72?.Te6edf3modem_presetTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3restoredTb4b4b4, Ta5d6ff"Ta5d6ffCaller notified once the retry succeededTa5d6ff"Tb4b4b4)
Te6edf3watcherJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffrestoreWatcherPublishesASingleNotificationWhenRestoreCompletesAfterForegroundTimeoutTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffx:LATE-RESTORETa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3addressTb4b4b4)
Te6edf3seedInterruptedSessionTb4b4b4(Te6edf3deviceAddress Tff7b72= Te6edf3addressTb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3failLocalConfigWritesRemaining Tff7b72= Tffa657IntTb4b4b4.Te6edf3MAX_VALUE
Te6edf3radioControllerTb4b4b4.Te6edf3onLocalConfigWriteAttempt Tff7b72= Tb4b4b4{
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3restored Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3watcherJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{ Te6edf3restored Tff7b72+Tff7b72= Tffa657it Tb4b4b4} Tb4b4b4}
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(Te6edf3DiscoveryHomeRestorerTb4b4b4.Te6edf3FOREGROUND_RESTORE_TIMEOUT_MS Tff7b72+ T79c0ff1Tb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3restoredTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffthe foreground timeout must not report an unfinished restoreTa5d6ff"Tb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3failLocalConfigWritesRemaining Tff7b72= T79c0ff0
Te6edf3radioControllerTb4b4b4.Te6edf3onLocalConfigWriteAttempt Tff7b72= Tff7b72null
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(Te6edf3DiscoveryHomeRestorerTb4b4b4.Te6edf3POST_RESTORE_SETTLE_DELAY_MSTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3restoredTb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffLONG_FASTTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3restoredTb4b4b4, Ta5d6ff"Ta5d6fflate completion must notify exactly onceTa5d6ff"Tb4b4b4)
Te6edf3watcherJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffrestoreInterruptedSessionsOnReconnectFinalizesPendingCompleteWithoutLegacyRestorePlanTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3addressTb4b4b4)
Te6edf3seedInterruptedSessionTb4b4b4(
Te6edf3deviceAddress Tff7b72= Te6edf3addressTb4b4b4,
Te6edf3completionStatus Tff7b72= Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3RESTORE_PENDING_COMPLETETb4b4b4,
Te6edf3homePreset Tff7b72= Ta5d6ff"Ta5d6ffCUSTOMTa5d6ff"Tb4b4b4,
Te6edf3homeLoraConfig Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3watcherJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{Tb4b4b4} Tb4b4b4}
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3COMPLETETb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTb4b4b4, Ta5d6ff"Ta5d6fflegacy row without a restore plan must not touch the radioTa5d6ff"Tb4b4b4)
Te6edf3watcherJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ffrestoreInterruptedSessionsOnReconnectTerminalizesUnrestorableSessionTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"
Te6edf3selectDeviceTb4b4b4(Te6edf3addressTb4b4b4)
T8b949e// Config was null at scan start (nothing to restore), so this session can never be recovered.
Te6edf3seedInterruptedSessionTb4b4b4(Te6edf3deviceAddress Tff7b72= Te6edf3addressTb4b4b4, Te6edf3homePreset Tff7b72= Ta5d6ff"Ta5d6ffCUSTOMTa5d6ff"Tb4b4b4, Te6edf3homeLoraConfig Tff7b72= Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3restored Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3engine Tff7b72= Te6edf3createEngineTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72val Te6edf3watcherJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3engineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{ Te6edf3restored Tff7b72+Tff7b72= Tffa657it Tb4b4b4} Tb4b4b4}
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
T8b949e// Marked terminal so it stops re-matching getInterruptedSession forever; radio untouched, no notification.
Te6edf3assertEqualsTb4b4b4(Te6edf3DiscoverySessionStatusTb4b4b4.Te6edf3UNRESTORABLETb4b4b4, Te6edf3discoveryDaoTb4b4b4.Te6edf3sessionsTb4b4b4.Te6edf3getValueTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3completionStatusTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3lastLocalConfigTb4b4b4, Ta5d6ff"Ta5d6ffNothing to write to the radio without a captured configTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3restoredTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffNo notification for an unrestorable sessionTa5d6ff"Tb4b4b4)
Te6edf3watcherJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e// endregion
Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3DEFAULT_DEVICE_ADDRESS Tff7b72= Ta5d6ff"Ta5d6ffx:TEST-DEVICETa5d6ff"
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.31s